Skip to content

Page Transitions

So, we've seen how to create multiple routes in Next, by creating page.js components and placing them in subdirectories.

We haven't yet seen how to connect these routes with links!

This is one of those topics that seems simple on the surface, but there's actually an incredible amount of sophisticated engineering involved. In this lesson, we're going to go deeper into how routing in Next works.

Video Summary

This project begins with two routes that link to each other:

  • The index page (/app/page.js) which links to the About page.
  • The About page (/app/about/page.js) which links to the index page.

I've set it up using the trusty anchor tag, <a href="/about">.

This works, but we're missing out on a pretty significant optimization!

As it stands, when we click on a link, we're performing a full page reload. First we download the server-generated HTML file, then the CSS files, then the JS.

These two pages, however, share 99% of the same stuff. They both have the same layout, the same CSS, the same JS dependencies. It feels wasteful to throw everything out and start from scratch when only a tiny amount of content has changed!

In the early 2010, frameworks like Angular.js and Ember became really popular. These frameworks had a radical new take on routing. Early Angular apps only had a single HTML file in the whole project. Routes were constructed using JavaScript, and we'd fetch chunks of code as-needed when the user clicks on a link.

This pattern is known as Single Page Applications (SPAs). We create the illusion of lots of separate HTML files by dynamically swapping out the parts that need to change. The result is much faster transitions between pages.

Client-side React applications (eg. Parcel with React Router) use this architecture. That's why, when we used Parcel, the application only had a single HTML file, /public/index.html!

Now, Next is not architected as a Single Page App. We generate lots of different HTML files with server-side rendering! But the clever devs on the Next team have figured out how to combine the benefits of SPA architecture with the benefits of SSR.

To take advantage, we need to use a special Link component:

import Link from 'next/link';
function Home() {
return (
<main>
<h1>Welcome to my website!</h1>
<p>
To learn more about this website, visit
our <Link href="/about">About Page</Link>.
</p>
</main>
);
}

This component is designed to be a drop-in replacement for the <a> tag. By swapping it out, we enable much faster client-side transitions.

Now when we click a link, we don't download a whole new HTML file. We download <1kb of JavaScript. Even with a light throttle of 5mbps, we can fetch the data we need in a small fraction of a second.

If we inspect the data, it looks like this:

0:[["children","__PAGE__",["__PAGE__",{}],"$L1",[[],["$L2",null]]]]
3:I{"id":"(app-client)/./node_modules/next/dist/client/link.js","chunks":["app/about/page:static/chunks/app/about/page.js"],"name":"","async":false}
1:[["$","main",null,{"children":[["$","h1",null,{"children":"Welcome to my website!"}],["$","p",null,{"children":["To learn more about this website, visit our ",["$","$L3",null,{"href":"/about","children":"About Page"}],"."]}]]}],null]
2:[[["$","meta",null,{"charSet":"utf-8"}],null,null,null,null,null,null,null,null,null,null,["$","meta",null,{"name":"viewport","content":"width=device-width, initial-scale=1"}],null,null,null,null,null,null,null,null,null,null,[]],[null,null,null,null],null,null,[null,null,null,null,null],null,null,null,null,null]

If we rummage through this code, we can see that it actually includes all of the information about the content we're dynamically swapping out. This is a JS representation of the new content!

If there's one thing React is good at, it's updating the DOM based on a JS sketch. With client-side routing, navigating to a new page is essentially a state change.

One more nice touch from the Next team: we start loading the new route when the user hovers over the link. Given that the content can download in ~60ms, there's a good chance that by the time the user actually clicks the link, the content will already be downloaded.

The wild thing about this to me is that each Next route has two "modes":

  1. It can serve an HTML file with all of the content.
  2. It can serve a JS representation of the page-specific content.

I was curious how this worked, so I opened Postman (opens in new tab), a tool for manually testing API endpoints. By looking through the headers sent in-browser, I was able to determine that the Rsc header is responsible for convincing the back-end to send either the HTML or the JS.

A note on JS navigation: You might think that because these links don't do a full page reload, it's not “real” navigation. Surely, there must be some downsides to artificially recreating routing in JavaScript?

As it happens, the browser has something called the PushState API (opens in new tab). This API allows us to interact with the browser's history/routing system in JS.

When we use the Link component, we actually are pushing a new state onto the history stack. The user can use the browser's Back/Forward buttons to move between pages.

And because Link renders an <a> tag, we can still do all the stuff we're used to with anchors (eg. right-click and "Open in New Tab").

Historically, the biggest downside with SPA navigation was that accessibility tools like screen readers struggled with it. They wouldn't necessarily notice when the route changed, since the HTML wasn't swapped out.

Fortunately, the Next team has addressed this. If you poke around in the Elements pane, you'll notice a mysterious element, <next-route-announcer>. This element doesn't affect anything for sighted users, but it uses ARIA tags to announce route changes to folks using screen readers and other assistive technologies.

For years now, there's been a common talking point:

React is a good tool for complex applications with lots of interactivity, but it's overkill for a static site.

They'll go on to say that developers who use React for smaller projects are being selfish, prioritizing their own developer experience over the end user experience. All of that framework stuff is bloating the bundle unnecessarily, and slowing down the performance.

Personally, I think it's a bit silly to shame developers for choosing to use a particular tool; I think most of us are thinking a lot about the end user experience, but there's so much more to UX than the # of kilobytes in the JS bundle.

But even if we buy into this argument that performance is the most important thing, I don't think it's true anymore that React is bad for performance, when you use something like Next!

  • Because Next uses Server Side Rendering, the initial HTML is delivered fully-formed. If we build our HTML at compile-time (more on this soon), it's the fastest possible solution to this problem.
  • It might take some time for the JS bundle to download, to make the site fully interactive, but most things should still work in the meantime. If you click a link before the JS bundle has downloaded, it'll still take you where you want to go!
  • Once the JS bundle has downloaded, all subsequent link clicks are blazing fast, way faster than a bunch of plain HTML files.

Something I forgot to mention in the video: JS bundles will only get smaller and smaller as more and more libraries lean into React Server Components!

I'm not saying that Next is the fastest framework in the world, but if anyone tells you that React is slow, they're likely relying on information that is years out of date.

You can poke around with the code from this video on Github:

There's some other stuff in there, which'll be covered in the lessons ahead. 😄